#--
# Copyright (C) 2009 HiveSight sprl
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License, version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
# for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#++
 
require 'logger'
require 'uri'
require 'net/http'
require 'digest/sha1'

module AppEngine
  # An extension for the Logger class. It writes log messages
  # to a AppServer log webapp using AppEngine::LogDevice. 
  #
  # Example:
  #
  # logger = AppEngine::Logger.new(AppEngine::LogDevice('my_ruby_log'))
  #
  class Logger < ::Logger

    def initialize(logdev)
      super(nil)
      @formatter = AppEngine::Formatter.new
      @logdev = logdev
    end
  
    # Rewrite of the add method to use LogDevice's ability to record severity outside the message itself
    # === Synopsis
    #
    #   Logger#add(severity, message = nil, progname = nil) { ... }
    #
    # === Args
    #
    # +severity+::
    #   Severity.  Constants are defined in Logger namespace: +DEBUG+, +INFO+,
    #   +WARN+, +ERROR+, +FATAL+, or +UNKNOWN+.
    # +message+::
    #   The log message.  A String or Exception.
    # +progname+::
    #   Program name string.  Can be omitted.  Treated as a message if no +message+ and
    #   +block+ are given.
    # +block+::
    #   Can be omitted.  Called to get a message string if +message+ is nil.
    #
    # === Return
    #
    # +true+ if successful, +false+ otherwise.
    #
    # When the given severity is not high enough (for this particular logger), log
    # no message, and return +true+.
    #
    # === Description
    #
    # Log a message if the given severity is high enough.  This is the generic
    # logging method.  Users will be more inclined to use #debug, #info, #warn,
    # #error, and #fatal.
    #
    # <b>Message format</b>: +message+ can be any object, but it has to be
    # converted to a String in order to log it.  Generally, +inspect+ is used
    # if the given object is not a String.
    # A special case is an +Exception+ object - it's message is sent as the message
    # part, and its class and backtrace are added as the log's stack_dump parameter.
    # See local version of #msg2str.
    #
    def add(severity, message = nil, progname = nil, exception = nil, &block)
      severity ||= UNKNOWN
      if @logdev.nil? or severity < @level
        return true
      end
      progname ||= @progname
      stack_dump = exception
      if message.nil?
        if block_given?
          message = yield
        else
          message = progname
          progname = @progname
        end
	  elsif message.is_a?(::Exception)
	    stack_dump = @formatter.format_exception(message)
      end
      res = @logdev.write(@formatter.format_message(message), 
						  progname,
						  @formatter.format_severity(severity), 
						  stack_dump)
      puts res.body
      true
    end
  
  end
  
  # Differs from the original formatter in that it doesn't add the timestamp and severity
  # to the formatted message.
  # Severity is added separately, and timestamp is assigned by the log server.
  class Formatter < Logger::Formatter
    Format = "%s: %s\n"

    def call(msg)
      msg2str(msg)
    end
	alias :format_message :call
    
    SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY)
    # Copied from the same private method in Logger to here, since we want to output
    # the severity label to the server separately form the message.
    def format_severity(severity)
      SEV_LABEL[severity] || 'ANY'
    end
	
	def format_exception(e)
	  "#{ e.class }\n" <<
        (e.backtrace || []).join("\n")
	end
  
	private 
	
	def msg2str(msg)
      case msg
      when ::String
        msg
      when ::Exception
        msg.message
      else
        msg.inspect
      end
    end
  end

  # A destination for Ruby's built-in Logger class. It writes log messages
  # to a AppServer log webapp. 
  #
  # Example:
  #
  # logger = Logger.new(AppEngine::LogDevice('my_ruby_log'))
  #
  class LogDevice

    DEFAULT_CAP_SIZE = (10 * 1024 * 1024)

    @@log_uri = nil

    class << self # Class methods

      # Return URI of the AppEngline log
      def log_uri
        raise "connection not defined" unless @@log_uri
        @@log_uri
      end

      # Set the AppEngline log uri. 
      # Pass either a URI object or a URL string
      def log_uri=(val)
        @@log_uri = (val.is_a? URI) ? val : URI.parse(val)
      end

    end

    # +name+ is the name of the Logger.
    # +options+ is a hash that may have the following entries:
    #
    # <code>:api_key</code> - Required. The identification key to use with the server.
    # <code>:secret_key</code> - Required. The secret key to use with the server.
    #
    def initialize(name, options = {})
      @logger_name = name
      @api_key = options[:api_key]
      @secret_key = options[:secret_key]
      raise "API key and secret key required" unless (@api_key && @secret_key)
    end

    # Write a log message to the AppEngine server.
    def write(message, progname, severity, stack_dump=nil)
      data = {	'd' => stack_dump,
				'l' => @logger_name,
				'm' => message,
				'p' => progname,
				's' => severity,
				'n' => rand(2**32-1).to_s, #nonce
			}
      sig = sign(data)
      data["sig"] = sig
      data["key"] = @api_key
      begin
        Net::HTTP.post_form(AppEngine::LogDevice.log_uri, data)
      rescue
        puts $!
      end
    end
	
    # Sign the data with the secret key
    def sign(data)
      s = data.to_a.sort {|p1,p2| p1[0] <=> p2[0] }.join
      s << @secret_key
      Digest::SHA1.hexdigest(s)
    end
    private :sign

    # Close the log. This method is a sham. Nothing happens as there's nothing to close. 
    # You can actually continue to use this LogDevice.
    def close
    end
    
  end
  
end
